﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Linq;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using Lucene.Linq.Utility;

namespace Lucene.Linq
{
    /// <summary>Index Set for a LINQ to SQL DataContext
    /// TODO: DC instance rebuilding see http://www.codeplex.com/linqtolucene/WorkItem/View.aspx?WorkItemId=6072
    /// </summary>
    /// <typeparam name="TDataContext">The DataContext type</typeparam>
    public class DatabaseIndexSet<TDataContext> : IndexSet
        where TDataContext : DataContext
    {
        #region Fields/Properties

        private readonly ReaderWriterLockSlim _dataContextLock =
            new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

        private TDataContext _dataContext;


        /// <summary>
        /// The data context instance. 
        /// Data Contexts should be recycled periodically based on unit-of-work basis.
        /// 
        /// </summary>
        public TDataContext DataContext
        {
            get { return _dataContext; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                using (_dataContextLock.WriteLock())
                {
                    _dataContext = value;
                }
            }
        }

        #endregion

        #region Ctors/Init

        /// <summary>Creates RAM Indexes from data context</summary>
        /// <param name="dataContext">The data context instance</param>
        public DatabaseIndexSet(TDataContext dataContext)
        {
            if (dataContext == null)
                throw new ArgumentNullException("dataContext");

            DataContext = dataContext;
            Init();
        }

        ///<summary>Creates File System Indexes from data context</summary>
        ///<param name="path">File System path</param>
        /// <param name="dataContext">The data context instance</param>
        public DatabaseIndexSet(string path, TDataContext dataContext)
            : base(path)
        {
            if (dataContext == null)
                throw new ArgumentNullException("dataContext");

            DataContext = dataContext;
            Init();
        }

        ///<summary>Creates file system indexes from data context</summary>
        ///<param name="directory">File system directory info</param>
        /// <param name="dataContext">The data context instance.</param>
        public DatabaseIndexSet(DirectoryInfo directory, TDataContext dataContext)
            : base(directory)
        {
            if (dataContext == null)
                throw new ArgumentNullException("dataContext");

            DataContext = dataContext;
            Init();
        }


        private void Init()
        {
            // get all tables in db context
            IEnumerable<Type> linqTableTypes = GetTableTypes();

            int linqTableCount = linqTableTypes.Count();

            // Throw if there are no tables
            if (linqTableCount == 0)
            {
                throw new ArgumentException("The data context type has no Tables");
            }

            // if there are any tables on the context, add them to the set
            foreach (Type linqTableType in linqTableTypes)
            {
                try
                {
                    Add(linqTableType);
                }
                catch (ArgumentException)
                {
                    // linq table type doesn't have Document attribute
                    // for now, skipping this table type makes sense
                }
            }
        }

        #endregion

        #region Private Methods

        private IEnumerable<Type> GetTableTypes()
        {
            Type iTableType = typeof(ITable);
            IEnumerable<Type> linqTableTypes = from prop in typeof(TDataContext).GetProperties()
                                               where prop.PropertyType.IsGenericType &&
                                                     iTableType.IsAssignableFrom(prop.PropertyType)
                                               select prop.PropertyType.GetGenericArguments()[0];

            return linqTableTypes;
        }

        private int GetTableRecordCount(ITable table, Type tableType)
        {
            Expression expr = Expression.Call(typeof(Queryable), "Count", new[] { tableType }, Expression.Constant(table));
            var count = table.Provider.Execute<int>(expr);
            return count;
        }

        private IEnumerable GetAllFromTable(ITable table, Type tableType)
        {
            ParameterExpression param = Expression.Parameter(tableType, "c");
            Expression selector = Expression.Parameter(tableType, "c");
            Expression pred = Expression.Lambda(param, param);

            IQueryable queryable = table;

            Expression expr = Expression.Call(typeof(Queryable), "Select", new[] { tableType, tableType },
                                              Expression.Constant(table), pred);
            IQueryable query = table.Provider.CreateQuery(expr);


            DbCommand cmd = _dataContext.GetCommand(query);
            Console.WriteLine("Generated Select * T-SQL:");
            Console.WriteLine(cmd.CommandText);
            Console.WriteLine();

            return _dataContext.ExecuteQuery(tableType, cmd.CommandText);
        }

        #endregion

        #region Public Methods

        /// <summary>Write all the records from the table type into their respective indexes</summary>
        /// <typeparam name="TTable">Table type to index</typeparam>
        public void Write<TTable>()
        {
            Write(typeof(TTable));
        }

        /// <summary>Write all the records from the table type into their respective indexes</summary>
        /// <param name="tableType">Table type to index</param>
        public void Write(Type tableType)
        {
            using (_dataContextLock.ReadLock())
            {
                IIndex index = Get(tableType);
                string name = tableType.Name;

                // Get the LINQ to SQL ITable instance
                ITable table = _dataContext.GetTable(tableType);
                if (table == null)
                    throw new ArgumentException("tableType doesnt belong to db");

                // get the number of records in the table
                int itemCount = GetTableRecordCount(table, tableType);

                // get all the records from the table
                IEnumerable items = GetAllFromTable(table, tableType);

                Console.WriteLine("About to write " + name + "s...");


                if (itemCount == index.Count)
                {
                    Console.WriteLine("Not adding " + name + "s, because index is up to date.");
                }
                else
                {
                    index.Add(items);
                    Console.WriteLine("Added " + index.Count + " " + name + "s.");
                }
            }
        }

        /// <summary>Write all the records from each table into their respective indexes</summary>
        public void Write()
        {
            using (_dataContextLock.ReadLock())
            {
                IEnumerable<Type> tableTypes = GetTableTypes();
                if (tableTypes.Any())
                {
                    foreach (Type tableType in tableTypes)
                    {
                        try
                        {
                            Write(tableType);
                        }
                        catch (ArgumentException)
                        {
                            // skip this table type
                        }
                    }
                }
            }
        }

        #endregion
    }
}